閱讀本篇文章前,仔細想想看
讀者認為目前對 TypeScript 編譯器的設定的了解程度如何呢?
如果還沒理解完畢的話,可以先翻看最近這幾天的文章喔!
今天的東西筆者認為讀者不一定會需要用 —— 因為 ES6 提供的 import
/ export
語法除了被 TypeScript 採用外,而且還可以被轉譯成不同的模組形式(利用 tsconfig.json
裡的 module
設定)。
但是筆者強烈建議讀者必須要知道 TypeScript Namespaces 的目的是為了能夠理解第三方套件到底如何融入到 TypeScript 專案!
在結合第三方套件時,如果遇到別的套件的型別定義有出現雷或者是錯誤,有時候可以藉由 TypeScript Namespaces 的機制,重新詮釋適合自己專案的型別定義與設定
畢竟使用 TypeScript 的宗旨就是善用 TypeScript 提供的型別系統(Type System)來協助專案的開發與維護。
本篇開始以後應該算是《戰線擴張》篇章系列中最重要的一段(不過讀者還是得知道編譯器設定的基礎 XD),因此我們從理解 TypeScript Namespaces —— 正文開始!
筆者剛開始學程式(不是指剛開始學 TypeScript 喔)聽到命名空間這個詞,完全不知道到底在搞什麼。看到 C++ Namespace 時,問了這個問題然後完全被一句話恍惚帶過:
喔~ 防止程式的衝突啊,不然咧?
(聽完當下很想直接揍那個人一拳)
事實上,筆者後來學程式久了之後,大概會這樣詮釋:
重點 1. Namespaces 的意義
由命名 Name 與空間 Space 兩個單字組成。(
廢話)命名空間主要的目的是組織並且包裝程式碼。
其中,每個人寫的程式碼、套件、框架等,在各種變數、函式、類別等等的取名上可能會有重複,譬如:某 A 與 B 套件可能同時用到名為
cache
的變數,這樣一來,如果兩個套件一起使用會產生嚴重的衝突 —— 來自於變數命名上的衝突。為了將不同的程式碼區塊進行隔離(防止
交叉感染交互污染),於是宣告名為 Namespace 的區塊 —— 每個程式碼、套件等可以建立起自己的空間,自由使用命名而不需要擔心會不會誤用到其他套件或程式碼已經命名過的變數、函式等。
首先,筆者本篇準備要示範的程式碼內容如下:
相信讀者應該很熟悉這些函式到底在做什麼,不過筆者還是簡單說明:
PI
為常數,代表圓周率AreaOfCircle
為計算圓面積的函式,必須填入一個參數 radius
代表圓的半徑AreaOfRectangle
為計算長方形面積的函式,必須填入兩個參數 width
與 height
代表長方形的邊長CircumferenceOfCircle
為計算圓周長的函式,必須填入一個參數 radius
代表圓的半徑CircumferenceOfRectangle
為計算長方形周長的函式,必須填入兩個參數 width
與 height
代表長方形的邊長筆者就不對以上的程式碼進行測試了,直接切入正題。
首先,以上的範例平常使用可能不會有太大的問題,僅僅只是計算幾合圖形的各種資訊而已。
然而,如果你引入各種不同的套件,譬如繪圖相關的套件,免不了會有跟圓或者是長方形相關的計算與變數等。
假設剛好引入的套件裡也含有 PI
這個變數,這樣就尷尬了 —— 它會跟剛剛的範例程式起衝突,尤其 PI
在以上的程式碼又是被定義為常數(Constant)。(不過現在要遇到會跟專案本身起衝突的套件應該幾乎沒有了,因為都可以被 IIFE 隔開掉)
於是我們希望可以建立起不同的命名空間防止污染到程式碼的全域,這時候本日主角 —— namespace
派上用場啦~
讀者應該可以看到,筆者只是簡簡單單地使用 namespace
關鍵字配上一個名稱 MyMath
宣告出一個命名空間,並且將所有的程式包裝起來。
通常要使用 namespace
裡面提供的功能,可以將 namespace
視為 JSON 物件的感覺(但 Namespaces 不是 JSON 物件!),使用點(.
)呼叫出命名空間內部的屬性與方法。不過,如果你想要呼叫出 MyMath.PI
的話還是會出現錯誤!(如圖一)
圖一:MyMath.PI
不屬於 MyMath
,這是怎麼一回事?
原來 —— 如果想要讓命名空間提供各種功能的話,必須使用 export
關鍵字 —— 因此筆者將所有的 MyMath
裡面的變數與方法進行輸出的動作。(實際上,讀者當然可以選擇輸出部分的功能)
這樣一來,TypeScript 的警告訊息就消失囉~(簡單測試結果如圖二)
圖二:簡單的測試結果,可以使用 MyMath
命名空間提供的各種功能
重點 1. 命名空間的宣告 TypeScript Namespaces Declaration
若想要宣告某命名空間
N
,該空間若想提供變數X
與方法M
,則宣告時,必須將X
與M
使用export
關鍵字進行輸出的動作。若想要使用命名空間
N
輸出的功能,則可以使用點.
來呼叫。
當然,命名空間有很彈性的功能 —— 可以使用巢狀式的命名空間來整理程式碼。如以下的範例:
很遺憾地,因為命名空間有嚴格的規則:
凡任何要從命名空間輸出的功能,必須要使用
export
關鍵字進行輸出
也就是說 —— 呼叫 MyMathV2.Circle
,光是這樣又會出現警告!(如圖三)
圖三:結果想要呼叫 MyMathV2.Circle
命名空間內的功能,但是因為 Circle
命名空間沒被輸出,所以被視為不存在
因此筆者快速將 export
標註在 Circle
與 Rectangle
上。(以下程式碼測試結果如圖四)
圖四:成功地使用 MyMathV2.Circle
與 MyMathV2.Rectangle
兩個不ㄧ樣的模組
重點 2. 巢狀命名空間 Nested Namespaces
可以運用巢狀命名空間將程式碼整理到不同的區塊,並且任何內部的命名空間要被外部使用時,必須標註
export
關鍵字。
export
關鍵字筆者再更近一步延伸 —— 型別、介面甚至是類別的宣告也必須使用 export
關鍵字!
因此使用以上的程式碼,測試後結果如圖五。
圖五:所以連型別系統的東西與類別都可以被存放在命名空間裡
重點 3. 命名空間可以輸出的功能 Exportable Utilities From Namespaces
只要是變數(Variables)、函式(Functions)、型別(Types,也包含 列舉型別 Enumerated Type)、介面(Interfaces)、類別(Classes)與命名空間(Namespaces)都是可以在命名空間裡使用
export
進行輸出。唯一不能輸出的是值(Value)本身。(例如:你不能在命名空間直接
export 123;
,這樣會出現警告)
跟介面的融合 Interface Merging 概念很像,不過官方統稱為宣告的融合 Declaration Merging。
其實就只是將 namespace
裡面的東西拆開來,但是命名空間的名稱都會是ㄧ樣的!所以剛剛在 MyMathV2
所列舉的例子跟以下 MyMathV3
是等效的!(圖六是以下的程式碼測試的結果)
圖六:測試 MyMathV3
的結果
不過這裡就要和讀者探討一個問題了:
如果想要在交互使用同個名稱的命名空間下,卻分配到不同的區塊的功能,會出現什麼樣的結果?(
這就是所謂的亂搞)
筆者為了減少討論案例的複雜度,以下會以 Circle
這個命名空間進行範例延伸討論:
以下的程式碼,筆者額外宣告同為 Circle
的命名空間 —— 其中,第二次宣告的命名空間有使用到第一個命名空間宣告的 PI
:
結果答案是:可以。(測試結果如圖七)
圖七:同個命名模組下,可以交互使用輸出的功能!
以下的程式碼,筆者取消掉第一次宣告的命名空間 Circle
裡面的 PI
的輸出:
結果答案是:不行。儘管是同個命名模組下的宣告,但是要能夠在不同地方使用功能,照樣得遵守輸出功能的原則 —— 在變數、函式等等要輸出的東西旁邊標註 export
關鍵字。(測試結果如圖八)
圖八:如果 PI
沒有被輸出,就算是在同名稱的命名空間下,不能使用 PI
以下的程式碼,筆者覆寫掉第一次宣告 Circle
命名空間時的 area
方法:
測試結果是:不行。因此重複宣告同一個函式是錯誤的。(測試結果如圖九)
圖九:答案是不能宣告重複的函式(Duplicate function implementations
)
但請讀者別急著下結論!
如果是介面(TypeScript Interface)這種具備可融合的特性:
讀者可以自行驗證,以上的程式碼在 TypeScript 裡面不會出現警告。
因此本題的答案是:不一定 —— 如果是一般變數、函式、類別甚至是型別(Type)的宣告就會出現類似 Duplicate declaration
相關的錯誤。
然而,因為介面具有可被融合的特性,儘管在不同區塊之同一命名空間下,介面也是可以被融合呢!
重點 4. 命名空間的融合 Namespace Merging
若某命名空間
N
在不同的地方有被重複宣告,則N
所提供的功能為所有不同地區宣告的N
輸出的功能進行聯集的結果。另外,不同區塊的
N
可以交互使用各自輸出的功能。然而,不同區塊的
N
不能覆蓋之前宣告的N
所提供的功能包含變數(Variables)、函式(Functions)、類別(Classes)與型別(Types),但介面(Interfaces)屬於例外 —— 因為介面具備融合的特性,這時候並不是覆蓋過往宣告的介面,而是和過往介面的宣告進行融合的動作。
最後的最後,筆者還是得額外提醒一下 —— 介面融合與命名空間融合的差別:
重點 5. 介面融合 Interfaces Merging V.S. 命名空間融合 Namespaces Merging
- 介面宣告的是規格;命名空間則是一系列功能的包裝,主要意義在於防止全域的污染
- 兩者皆可以動態擴充,然而擴充的意義不同 —— 介面擴充的意義就是規格的擴充;命名空間則是包含介面,任何功能的擴充(如:變數、函式、類別等等的宣告)
- 介面可以進行函式的超載;然而,命名空間因為單純只是對實際的功能進行輸出的動作,因此函式自然根本不會有什麼超載或者是很神奇的功能(
不過這個應該是廢話)- 命名空間裡面可以包含介面的宣告與輸出,反之則不行(
這應該也是廢話,誰會在介面裡面宣告 Namespace?)
今天主要是先讓讀者熟悉基本的 TypeScript Namespaces 的語法特性,因為下一篇就要進到更難纏一點的單元 —— 將程式碼分配到不同的檔案進行載入的動作 —— 這是學習開發基本 TypeScript 專案的基礎之一;然而,就廣義層面來說,讀者如果有微調第三方套件的模組所宣告的型別定義檔(Definition File)的需求,那麼 TypeScript Namespaces 的概念可就真的得打好基礎呢,這後面會再提到。
另外,讀者可能也會對於 TypeScript Namespaces 與 ES6 Import/Export 的使用差別感到疑惑,不過這一點下一篇的後面會講到 Namespaces 的由來,應該就會知道為何會多出這個東西了~
每次看 Maxwell的文章都有恍然大悟的感覺,期待後續~
我這邊有卡關個兩三天 XD 不過後面就是憑經驗寫的,所以沒有說很有把握可以寫好這部分
嗨~
關於「3. 同個命名空間下,能不能覆寫前一個命名空間輸出的功能?」做個補充:
這邊我認為不是能不能在 namespace 下覆蓋的問題,比較像是回到 js 的基本,
在 js 中,無法重複定義同個名稱的 function,若是你將範例改成 var 就可以覆蓋了。
(可以想像成在同 namespace 中,等於沒有用 namespace 隔離開 export 的內容)
// EX.
namespace N {
export var x = 1
}
namespace N {
export var x = 2
}
console.log(N.x) // 2